package cn.wp.ewallet.wallet.extension.util;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.Transfer;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * 钱包交易相关API处理工具类
 * guoxiaoyang
 * Created by guoxiaoyang on 2020/1/11
 */
public class EwalleTransactionUtil {

    private static final Logger log = LoggerFactory.getLogger(EwalleTransactionUtil.class);

    private static EwalleTransactionUtil ewalleTransactionUtil;

    public static void main(String[] args) throws Exception {

        EwalleTransactionUtil walletUtil = new EwalleTransactionUtil();
        //以太坊账户地址
        String ethAddress = "0xc7adf71adfac274a20037acabc3809e48e02def3";
        //shqc账户地址
        String shqcAddress = "0x333d953d13c8c37a38ebea174835aafc8e57689d";
        //usdt账户地址
        String usdtAddress = "0x28f635f5f4373559e1db437d7002d386cf718338";
        //shqc合约地址
        String shqcConnectAddress = "0x9317312011821ddaee0684212b09e97d6d109a77";
        //usdt合约地址
        String usdtConnectAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7";

        //infura 提供的主网客户端地址
        String mainNet = "https://mainnet.infura.io/v3/e22435af4fc24b16be66aa4c3bb41286";
        //infura提供的测试网客户端地址
        String rinkebyNet = "https://rinkeby.infura.io/v3/e22435af4fc24b16be66aa4c3bb41286";

        Web3j mainWeb3j = walletUtil.getDefaultConect(mainNet);

        Web3j rinkebyWeb3j = walletUtil.getDefaultConect(rinkebyNet);


        /**
         *  shqc代币交易hex唯一标示:0x4680700213a402984ec0ca50347161bcdb08b5634ba58e15589abbb52fe224ec
         * 发送方0xc7adf71adfac274a20037acabc3809e48e02def3
         * 接收方0x333d953d13c8c37a38ebea174835aafc8e57689d
         * 数量2
         * 币种SHQC
         * 合约地址：0x9317312011821ddaee0684212b09e97d6d109a77
         */
        String shqcTransactionHex = "0x3b1782d4519f77743bd020be86707da01e2a3778efa4de5000686757d4411bb0";

        try {
            EthGetTransactionReceipt receipt = walletUtil.getTransactionReceiptByHex(mainWeb3j,shqcTransactionHex);
            TransactionReceipt result = receipt.getResult();
            log.info("shqc交易查询:\n" +
                    "发送方 ：{}\n" +
                    " 接收方 ：{}\n" +
                    "合约地址 ：{}\n" +
                    "状态 ：{}\n" +
                    "Gas 消耗:{}Gwei\n",result.getFrom(),result.getLogs().get(0).getTopics().get(2),result.getTo(),result.getStatus().substring(2),result.getGasUsed());
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         *  usdt代币交易hex唯一标示 这笔交易目前已成功，
         *  发送方0x078f20543048c7c2c298f9e9cb705267e56d16a6
         *  接收方0x2ddcfb65738426a9626209849480be07daf6764f
         *  数量7,288
         *  币种USDT
         */
        String usdtTransactionHex = "0x4680700213a402984ec0ca50347161bcdb08b5634ba58e15589abbb52fe224ec";
        try {
            EthGetTransactionReceipt receipt = walletUtil.getTransactionReceiptByHex(mainWeb3j,usdtTransactionHex);
            TransactionReceipt result = receipt.getResult();
            log.info("usdt交易查询:\n" +
                    "发送方 ：{}\n" +
                    " 接收方 ：{}\n" +
                    "合约地址 ：{}\n" +
                    "状态 ：{}\n" +
                    "Gas 消耗:{}Gwei\n",result.getFrom(),result.getLogs().get(0).getTopics().get(2),result.getTo(),result.getStatus().substring(2),result.getGasUsed());
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         *eth交易hex唯一标示:0xd6428842dd11a1f0bf2eaeb0e25d86a3267a27c932b3750f17e817af513af67e
         * 接收方:
         * 0xd12f19046be165cc01dff33e3d40ba7ab2e0a9e3
         * 金额:
         * 2.00697 ETH | ￥1,990.23
         * 手续费:
         * 0.00021 ETH | ￥0.21
         */
        String ethTransactionHex = "0xd6428842dd11a1f0bf2eaeb0e25d86a3267a27c932b3750f17e817af513af67e";
        try {
            EthGetTransactionReceipt receipt = walletUtil.getTransactionReceiptByHex(mainWeb3j,ethTransactionHex);
            TransactionReceipt result = receipt.getResult();
            log.info("eth交易查询:\n" +
                    "发送方 ：{}\n" +
                    " 接收方 ：{}\n" +
                    "状态 ：{}\n" +
                    "Gas 消耗:{}Gwei\n",result.getFrom(),result.getTo(),result.getStatus().substring(2),result.getGasUsed());
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         * 以太币转账测试
         */
        String toEthAddressOfRinkebyNet = "0xa3A2e95B18953ef4DBB240D020D81d7d4e1aB355";
        String password = "zcxn2018";
        String acctFilePath = "f:/mywallet/UTC--2020-01-03T07-10-00.400000000Z--333d953d13c8c37a38ebea174835aafc8e57689d.json";
        Credentials credentials1 = walletUtil.loadWallet(acctFilePath, password);
        String txnHash = walletUtil.transto(rinkebyWeb3j,toEthAddressOfRinkebyNet,credentials1,BigDecimal.valueOf(10));
        log.info("以太币转账测试,txnhash is {}",txnHash);



        /**
         * gxyt代币交易测试
         */
        Credentials credentials =Credentials.create("8A5B2749CF594B62C88FBA5210009A05FF8CC6ED7CAF97821B15A7F5A9C58FC7");
        //账户地址
        String from = "0xDb80871890C74269715DaF2D65be4143048CB72A";
        String to = "0x985Ce3D1EdD69100a094535Cf6233c2F992DcE25";
        String gxytConnectAddress = "0x18c37106a9de39231f28e813c3a9ac4976fb8b1c";
        BigInteger amount = BigInteger.valueOf(10000);
        String tokenTxnHash = walletUtil.transferERC20Token(rinkebyWeb3j,from,to,amount,credentials,gxytConnectAddress);
        log.info("gxyt转账测试,tokenTxnHash is {}",tokenTxnHash);

        walletUtil.shutdownWeb3j(rinkebyWeb3j);
        walletUtil.shutdownWeb3j(mainWeb3j);
    }

    /**
     * 实例化工具类
     * @return
     */
    public static EwalleTransactionUtil getInstance(){
        if (null == ewalleTransactionUtil) {
            ewalleTransactionUtil = new EwalleTransactionUtil();
        }
        return ewalleTransactionUtil;
    }
    /**
     *
     * @param clientUrl 客户端/本地节点地址 如infura地址：https://mainnet.infura.io/v3/e22435af4fc24b16be66aa4c3bb41286
     * @return Web3j 以太坊客户端连接对象
     */
    public Web3j getDefaultConect(String clientUrl){
        Web3j web3j = Web3j.build(new HttpService(clientUrl));
        return web3j;
    }


    /**
     * 通过keystore文件加载钱包
     * @param walleFilePath
     * @param password
     * @return
     * @throws IOException
     * @throws CipherException
     */
    public Credentials loadWallet(String walleFilePath, String password) throws IOException, CipherException {
        Credentials credentials = WalletUtils.loadCredentials(password, walleFilePath);
        String address = credentials.getAddress();
        BigInteger publicKey = credentials.getEcKeyPair().getPublicKey();
        BigInteger privateKey = credentials.getEcKeyPair().getPrivateKey();
        log.info("address=" + address);
        log.info("public key=" + publicKey);
        log.info("private key=" + privateKey);

        return credentials;
    }

    /**
     *
     * @param web3j 要关闭的客户端连接对象
     */
    public void shutdownWeb3j(Web3j web3j)
    {
        web3j.shutdown();
    }
    /**
     * 查询以太币余额
     * @param web3j
     * @param address
     * @throws IOException
     * @return 以太币余额，单位为wei
     */
    public BigInteger getBlanceOf(Web3j web3j, String address) throws IOException {

        //第二个参数：区块的参数，建议选最新区块
        EthGetBalance balance = web3j.ethGetBalance(address, DefaultBlockParameter.valueOf("latest")).send();
        return balance.getBalance();
    }
    /**
     * 查询代币余额
     * @param web3j 以太坊客户端连接对象
     * @param fromAddress 对应要查询的账户地址
     * @param contractAddress 代币合约地址
     * @return 代币币种最小单位的int值，若返回NULL则查询失败
     */
    public  BigInteger getTokenBalance(Web3j web3j, String fromAddress, String contractAddress) throws IOException {
        //ERC20 智能合约函数名称
        String methodName = "balanceOf";
        List<Type> inputParameters = new ArrayList<>();
        List<TypeReference<?>> outputParameters = new ArrayList<>();
        Address address = new Address(fromAddress);
        inputParameters.add(address);
        TypeReference<Uint256> typeReference = new TypeReference<Uint256>(){};
        outputParameters.add(typeReference);
        Function function = new Function(methodName, inputParameters, outputParameters);
        String data = FunctionEncoder.encode(function);
        Transaction transaction = Transaction.createEthCallTransaction(fromAddress, contractAddress, data);
        EthCall ethCall;
        BigInteger balanceValue = BigInteger.ZERO;
        ethCall = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send();
        if (null != ethCall && StringUtils.isNotEmpty(ethCall.getValue())) {
            List<Type> results = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
            if(results != null && !results.isEmpty()) {
                log.info(results.toString());
                balanceValue = (BigInteger) results.get(0).getValue();
            }
        }

        return balanceValue;
    }


    /**
     * 查询以太坊交易收据,返回对象EthGetTransactionReceipt.getResult.getStauts()返回为0x1为成功，其余为失败
     * @param web3j
     * @param transactionHex
     * @return EthGetTransactionReceipt
     * @throws IOException
     */
    public EthGetTransactionReceipt getTransactionReceiptByHex(Web3j web3j,String transactionHex) throws IOException {
        return web3j.ethGetTransactionReceipt(transactionHex).send();
    }

    /**
     * 查询以太坊交易信息
     * @param web3j
     * @param transactionHex
     * @return
     * @throws IOException
     */
    public EthTransaction getTransactionByHex(Web3j web3j, String transactionHex) throws IOException {
        return web3j.ethGetTransactionByHash(transactionHex).send();
    }


    /**
     * 以太币转账
     * @param web3j 连接对象
     * @param address_to 转入地址
     * @param credentials 证书
     * @param value 转账金额 这里单位用GWEI
     * @throws Exception
     */
    public String transto(Web3j web3j, String address_to, Credentials credentials,BigDecimal value) throws Exception {
        TransactionReceipt send = Transfer.sendFunds(web3j, credentials, address_to,value, Convert.Unit.GWEI).send();
        return send.getTransactionHash();
    }

    /**
     * 估算手续费上限
     * @param web3j
     * @param transaction
     * @return
     */
    public BigInteger getTransactionGasLimit(Web3j web3j, Transaction transaction) {
        try {
            EthEstimateGas ethEstimateGas = web3j.ethEstimateGas(transaction).send();
            if (ethEstimateGas.hasError()){
                throw new RuntimeException(ethEstimateGas.getError().getMessage());
            }
            return ethEstimateGas.getAmountUsed();
        } catch (IOException e) {
            throw new RuntimeException("net error");
        }
    }

    /**
     * 获取账户的Nonce
     * @param web3j
     * @param addr
     * @return
     */
    public  BigInteger getNonce(Web3j web3j, String addr) {
        try {
            EthGetTransactionCount getNonce = web3j.ethGetTransactionCount(addr,DefaultBlockParameterName.LATEST).send();
            if (getNonce == null){
                throw new RuntimeException("net error");
            }
            return getNonce.getTransactionCount();
        } catch (IOException e) {
            throw new RuntimeException("net error");
        }
    }

    /**
     * 构建代币易信息
     * @param from
     * @param to
     * @param value
     * @param nonce
     * @param gasPrice
     * @param contractAddress
     * @return
     */
    public Transaction getGasLimitTransaction(String from,String to,BigInteger value,BigInteger nonce,BigInteger gasPrice,String contractAddress){
        // 构建方法调用信息
        String method = "transfer";
        // 构建输入参数
        List<Type> inputArgs = new ArrayList<>();
        inputArgs.add(new Address(to));
        inputArgs.add(new Uint256(value));

        // 合约返回值容器
        List<TypeReference<?>> outputArgs = new ArrayList<>();
        String funcABI = FunctionEncoder.encode(new Function(method, inputArgs, outputArgs));
        Transaction transaction = Transaction.createFunctionCallTransaction(from, nonce, gasPrice,null, contractAddress, funcABI);
        return transaction;
    }

    /**
     *构建代币易信息
     * @param web3j
     * @param from
     * @param to
     * @param value
     * @param contractAddress
     * @return
     * @throws IOException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public Transaction getGasLimitTransaction(Web3j web3j, String from,String to,BigInteger value, String contractAddress) throws IOException, ExecutionException, InterruptedException {

        //获取nonce，交易笔数
        BigInteger nonce;
        EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send();
        if (ethGetTransactionCount == null) {
            throw new RuntimeException("未查询到nonce");
        }
        nonce = ethGetTransactionCount.getTransactionCount();
        //gasPrice和gasLimit 都可以手动设置
        BigInteger gasPrice;
        EthGasPrice ethGasPrice = web3j.ethGasPrice().sendAsync().get();
        if (ethGasPrice == null) {
            throw new RuntimeException("未查询到gasprice");
        }
        gasPrice = ethGasPrice.getGasPrice();

        // 构建方法调用信息
        String method = "transfer";
        // 构建输入参数
        List<Type> inputArgs = new ArrayList<>();
        inputArgs.add(new Address(to));
        inputArgs.add(new Uint256(value));

        // 合约返回值容器
        List<TypeReference<?>> outputArgs = new ArrayList<>();
        String funcABI = FunctionEncoder.encode(new Function(method, inputArgs, outputArgs));
        Transaction transaction = Transaction.createFunctionCallTransaction(from, nonce, gasPrice,null, contractAddress, funcABI);
        return transaction;
    }

    /**
     * erc20代币转账
     * @param web3j 连接对象
     * @param from            转账地址
     * @param to              收款地址
     * @param value           转账金额
     * @param contractAddress 代币合约地址
     * @param credentials  证书
     * @return 交易哈希
     * @throws ExecutionException
     * @throws InterruptedException
     * @throws IOException
     */
    public String transferERC20Token(Web3j web3j,String from, String to, BigInteger value,Credentials credentials, String contractAddress) throws ExecutionException, InterruptedException, IOException {
        //获取nonce，交易笔数
        BigInteger nonce;
        EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.PENDING).send();
        if (ethGetTransactionCount == null) {
            throw new RuntimeException("未查询到nonce");
        }
        nonce = ethGetTransactionCount.getTransactionCount();
        //gasPrice和gasLimit 都可以手动设置
        BigInteger gasPrice;
        EthGasPrice ethGasPrice = web3j.ethGasPrice().sendAsync().get();
        if (ethGasPrice == null) {
            throw new RuntimeException("未查询到gasprice");
        }
        gasPrice = ethGasPrice.getGasPrice();

        // 获得余额
        BigInteger ethBalance = getBlanceOf(web3j, from);
        BigInteger tokenBalance = getTokenBalance(web3j, from, contractAddress);
        BigInteger balance = Convert.toWei(ethBalance.toString(), Convert.Unit.ETHER).toBigInteger();

        //构建估算gas消耗上限要用的交易信息
        Transaction transaction = this.getGasLimitTransaction(from,to,value,nonce,gasPrice,contractAddress);
        BigInteger gasLimit = getTransactionGasLimit(web3j, transaction);

        log.info("以太币余额:{},代币余额:{},gasLimit:{}.gasPrice:{}",ethBalance,tokenBalance,gasLimit,gasPrice);
        if (balance.compareTo(gasLimit) < 0) {
            throw new RuntimeException("手续费不足，请核实");
        }
        if (tokenBalance.compareTo(value) < 0) {
            throw new RuntimeException("代币不足，请核实");
        }
        //ERC20代币合约方法
        Function function = new Function(
                "transfer",
                Arrays.asList(new Address(to), new Uint256(value)),
                Collections.singletonList(new TypeReference<Type>() {
                }));
        //创建RawTransaction交易对象
        String encodedFunction = FunctionEncoder.encode(function);
        RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit,
                contractAddress, encodedFunction);

        //签名Transaction
        byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        String hexValue = Numeric.toHexString(signMessage);
        //发送交易
        EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
        String hash = ethSendTransaction.getTransactionHash();
        if (hash != null) {
            return hash;
        }
        return null;
    }

}
